// Font size in pixels for label text.
var fontPixels = 10;
function sizeFont(delta) {
  fontPixels += delta;
  fontPixels = Math.max(4, fontPixels);
}
function fontFormat() {
  return fontPixels + 'px sans-serif';
}

// Store scores as duples of [possible, scored] points.
var points = {possible: 0, scored: 1};  // array indexes
// The duples are arranged in an array by student group/aggregation.
var students = {all: 0, state: 1};  // array indexes
var studentSets = ['all', 'state'];

function setAggregation(set, node) {
  // Click event handler.
  var test = domRoot(node).test;
  test.aggr = students[set];
  refresh();
  return null;
}

var subjName = { 10: 'English Language Arts', 20: 'Mathematics', 30: 'K-8 Science, Technology, Engineering', 31: 'Biology', 32: 'Chemistry', 33: 'Physics' };

function labelColor(node) {
  if (colorEnabled())
    return (node.sumScore[points.scored] / node.sumScore[points.possible] < 0.6) ? 'lightgray' : 'black';
  else
    return 'black';
}

// Return the first property of an object.
function firstProp(obj) {
  for (var p in obj)
    return p;
}

// Return the depth of the given node. [Unused?]
function nodeDepth(node) {
  var depth = 0;
  while (node.parentNode) {
    depth++;
    node = node.parentNode;
  }
  return depth;
}

// Control which levels of the subtopic hierarchy are displayed.
// Level 0 is always present and never displayed. It has no nodeName.
// [Level 1 must always be present too for now.]
// [Consider making initial depth the key.]
var level = {root: {depth: 0, visible: false}, 
             subject: {depth:1, visible: true},
             strand: {depth:2, visible: true},
             standard: {depth:3, visible: true},
             objective: {depth:4, visible: false},
             item: {depth:5, visible: true}};

function depthLabel(depth) {
  // Return the property label for this depth.
  // Return '' for an invalid depth.
  for (var l in level)
    if (level[l].depth == depth)
      return l;
  return '';
}

function nextLevelHidden(topicLevel) {
  var nextDepth = level[topicLevel].depth + 1;
  var nextLevel = depthLabel(nextDepth);
  return nextLevel != '' && !level[nextLevel].visible;
}

function cutNode(node) {
  while (node.firstChild)
    node.parentNode.appendChild(node.firstChild);
  node.parentNode.removeChild(node);
}

function filterLevels(root) {
  var cutList = [];
  for (var lv in level) {
    if (lv != 'root' && !level[lv].visible)
      root.visitAfter(function(node) { if (node.topicLevel == lv) cutList.push(node) });
    while (cutList.length)
      cutNode(cutList.pop());
  }
}

function labelLevel(node, level) {
  // Set a label property to a level name.
  node.topicLevel = depthLabel(level);
}

function closeLevel(node) {
  // Close a level, on request from active visualizations.
  // Get the topic level from the node level property. Don't use the DOM level.
  level[node.topicLevel].visible = false;
  refresh();
  return null;
}

function reopenLevel(node) {
  level[depthLabel(level[node.topicLevel].depth + 1)].visible = true;
  refresh();
  return null;
}

// Determine if a node is leftmost in its level.
function leftmostNode(node) {
  var n = node;
  while (n.parentNode) {
    if (n.parentNode.firstChild !== n)
      return false;
    n = n.parentNode;
  }
  return !n.parentNode;
}

// Display test item text, in a separate window.
var itemTextWindow;
function displayItemText(node) {
  itemTextWindow = window.open(node.nodeValue.text, 'item_text_window');
  return null;
}

function sumSiblings(node, sum) {
  sum = [sum[0] + node.sumScore[0], sum[1] + node.sumScore[1]];
  if (node.nextSibling)
    return sumSiblings(node.nextSibling, sum);
  else
    return sum;
}

function addPerformance(node, aggr) {
  if (node.nodeValue)
    node.sumScore = node.nodeValue.scores[aggr];
  else if (node.firstChild)
    node.sumScore = sumSiblings(node.firstChild, [0, 0]);
  else
    node.sumScore = [0, 0]
}

function c(node) {
  // A color scale for fraction of points scored.
  if (!colorEnabled()) return 'tan';
  var frac = node.sumScore[points.scored] / node.sumScore[points.possible];
  return 'rgb(0,' + (Math.floor(frac * 255)) + ',0)';
}

function tip(node) {
  // Text for a tooltip, small info window appearing when the mouse hovers over a node.
  var text = node.nodeName;
  if (node.topicLevel == 'item') text = 'Item ' + text + ' (' + node.nodeValue.type + ')';
  text += ': ' + Math.round(node.sumScore[points.scored] / node.sumScore[points.possible] * 100) + '%';
  return text;
}

// Construct a new test object.
function Test(subj, grade, year, aggr, sort) {
  this.subject = subj;
  this.grade = grade;
  this.year = year;
  this.scores = scores[subj][grade][year];
  this.aggr = aggr;  // see students map
  this.sort = sort === undefined ? 'normal' : sort ;  // normal or performance
  this.domify = domify;
  this.copy = function() {
    return new Test(this.subject, this.grade, this.year, this.aggr, this.sort);
  }
}

function subtractBelow(node) {
  // Click event handler, subtract test below.
  var test = domRoot(node).test;
  var i = this.parent.parent.parent.parent.index,
      cols = tests[0].length,
      row = Math.floor(i / cols),
      col = i % cols;
  test.subtract = tests[row + 1][col];
  refresh();
  return null;
}

// Find the root of a DOM tree from any node.
function domRoot(node) {
  while (node.parentNode)
    node = node.parentNode;
  return node;
}

// Create a DOM tree for subtopics in a test.
function domify() {
  var that = this;
  this.scores = scores[this.subject][this.grade][this.year];
  // We'll add a dom property to the test object.
  this.dom = pv.dom(this.scores.items)
    .leaf(function(node) { return 'scores' in node })
    .root()
    ;
  this.dom.test = this;  // back link
  // Filter question types.
  this.dom.visitAfter(filterQuestionTypes);
  while (filteredQuestions.length) {
    var n = filteredQuestions.pop();
    n.parentNode.removeChild(n);
  }
  // Add performance data to each level (points possible, points scored).
  var aggr = this.aggr;
  this.dom.visitAfter(function(node) { addPerformance(node, aggr) });
  // Label levels before removing any to avoid mixups.
  this.dom.visitBefore(labelLevel);
  // Remove selected levels.
  filterLevels(this.dom);
  // Sort each level by performance or regular.
  this.dom.sort(function(a, b) { 
      if (document.getElementById('normal_sort_option').checked) {
        if (a.topicLevel == 'item')
          return a.nodeName - b.nodeName;
        else
          return a.nodeName > b.nodeName ? 1 : -1;
      } else {
        return a.sumScore[points.scored] / a.sumScore[points.possible] 
             - b.sumScore[points.scored] / b.sumScore[points.possible] 
      }
   })
}

// Establish a visualization element and a root pv.Panel.
function setupRoot() {
  // First, size our visualization block.
  var div = document.getElementById('cedu_vis');
  var w = document.body.clientWidth,
      h = window.innerHeight - 80;
  div.style.width = w + 'px';
  div.style.height = h + 'px';
  // Clear any old visualization.
  while (div.firstChild) div.removeChild(div.firstChild);
  // Create a root panel.
  rootPanel = new pv.Panel()
    .canvas('cedu_vis')
    .width(w)
    .height(h - 10)
    .bottom(10)
  ;
}

// Create a grid of test visualizations.
function visGrid() {
  // Set up a root panel.
  setupRoot();
  // Add a grid of icicle visualizations to the root panel.
  var grid = rootPanel.add(pv.Layout.Grid)
    .rows(tests)
    ;
  // Add an icicle visualization.
  var cellPanel = grid.cell.add(pv.Panel);
  var icicle = cellPanel.add(pv.Layout.Partition.Fill)
    // Omit the test root, leaving the single subject node as root.
    .nodes(function(test) { return test.dom.nodes().slice(1) })  
    // Node size represents number of points possible.
    .size(function(node) { return node.sumScore[points.possible] })
    .orient('top')
    .left(10)
    .right(10)
    .top(10)
    .bottom(10)
    ;
  // Add controls to duplicate a chart down or right.
  cellPanel.anchor('bottom').add(pv.Dot)
    .shape('triangle')
    .strokeStyle('gray')
    .fillStyle('lightgray')
    .radius(6)
    .event('click', dupDown)
  ;
  cellPanel.anchor('right').add(pv.Dot)
    .shape('triangle')
    .angle(- Math.PI / 2)
    .strokeStyle('gray')
    .fillStyle('lightgray')
    .radius(6)
    .event('click', dupRight)
  ;
  // Color represents performance (points scored / points possible).
  var nodeBar = icicle.node.add(pv.Bar)
    .fillStyle(c)
    // Nodes above item level have borders.
    .strokeStyle(function(node) { return (node.nodeValue === undefined) ? 'gray' : null })
    .lineWidth(1)
    .title(tip)  // information displayed when pointer hovers over node
  ;
  // Click on item node to open a window displaying test item text, as made available by ESE.
  nodeBar.add(pv.Panel)
    .visible(function(node) { return node.topicLevel == 'item' })
    .fillStyle(null)
    .strokeStyle(null)
    .events('all')
    .event('click', displayItemText)
  ;
  // Node labels above item level consist of a dot and the topic name.
  nodeBar.anchor('center')
    .add(pv.Dot)
    .visible(function(node) { return node.topicLevel != 'item' && node.topicLevel != 'subject' && labelsVisible() })
    .strokeStyle('rgb(60,60,60)')
    .radius(1)
      .anchor('top')
      .add(pv.Label)
      .text(function(node) { return node.nodeName })
      .textStyle(labelColor)
      .font(fontFormat)
      .textAngle(function(node) { return node.topicLevel == 'subject' ? 0 : - Math.PI / 4 })
  ;
  // Add info and controls to the subject bar.
  nodeBar.add(pv.Panel)
    .visible(function(node) { return node.topicLevel == 'subject' })
    .fillStyle(null)
      .anchor('top')
      .add(pv.Label)
      .text(function(node) { var test = domRoot(node).test; return test.year + ' Grade ' + test.grade + ' ' + subjName[test.subject]; })
      .font('14px sans-serif')
        .add(pv.Label)
        .text('Northampton')
        .top(15)
    .parent
      .add(pv.Panel)
      .width(function() { return this.parent.width() / 4 })
        .add(pv.Panel)
        .data(studentSets)
        .top(function(set) { return pv.Scale.ordinal(studentSets).splitBanded(15, this.parent.height())(set) })
        .height(function() { return this.parent.height() / studentSets.length })
        .events('all')
        .event('click', setAggregation)
          .anchor('left')
          .add(pv.Label)
          .font(function(set, set1, node) { return students[set] == domRoot(node).test.aggr ? '14px sans-serif' : '10px sans-serif' })
          .left(10)
        .parent
      .parent
        .anchor('left')
        .add(pv.Label)
        .top(0)
        .textBaseline('top')
        .text('student aggregation (click to select):')
        .font('12px sans-serif')
      .parent
        .anchor('right')
        .add(pv.Label)
        .text('Subtract below')
        .events('all')
        .event('click', subtractBelow)
  ;
  // Add controls to close levels.
  icicle.node.add(pv.Panel)
    .visible(function(node) { return node.topicLevel != 'subject' && leftmostNode(node) })
    .fillStyle(null)
    .strokeStyle(null)
      .anchor('left')
      .add(pv.Dot)
      .fillStyle('lightgray')
      .strokeStyle('gray')
      .radius(6)
      .left(-1)
      .event('click', closeLevel)
        .anchor('center')
        .add(pv.Label)
        .text('-')
        .font('12px sans-serif')
  ;
  // Add controls to reopen levels.
  icicle.node.add(pv.Panel)
    .visible(function(node) { return nextLevelHidden(node.topicLevel) && leftmostNode(node) })
    .fillStyle(null)
    .strokeStyle(null)
      .add(pv.Dot)
      .fillStyle('lightgray')
      .strokeStyle('gray')
      .radius(6)
      .left(-1)
      .bottom(0)
      .event('click', reopenLevel)
        .anchor('center')
        .add(pv.Label)
        .text('+')
        .font('12px sans-serif')
  ;
  // Label item nodes with item numbers.
  icicle.label.add(pv.Label)
    .visible(function(node) { return node.topicLevel == 'item' && labelsVisible() })
    .textStyle(labelColor)
    .font(fontFormat)
    .textAngle(- Math.PI / 4)
  ;
}

// Load time.

var displayLabelsCheckbox = document.getElementById('display_labels_check');
function labelsVisible() { return displayLabelsCheckbox.checked }
var colorCheckbox = document.getElementById('color_check');
function colorEnabled() { return colorCheckbox.checked }
function displayQuestionTypes() {
  var types = [];
  //if (document.getElementById('display_mc_check').checked) 
  types.push('MC');
  if (document.getElementById('display_or_check').checked) types.push('OR');
  //if (document.getElementById('display_sa_check').checked) 
  types.push('SA');
  return types;
}
var filteredQuestions = [];
function filterQuestionTypes(node) {
  if (node.nodeValue && displayQuestionTypes().indexOf(node.nodeValue.type) == -1) {
    filteredQuestions.push(node);
  }
}

if (!Modernizr.svg) {  // display apology for browser that doesn't support SVG

document.getElementById('cedu_controls').innerHTML = '<hr/>';  // replace controls
document.getElementById('cedu_vis').innerHTML = 
  '<div style="font-family: sans-serif">' +
    '<h2>About your web browser</h2>' +
    '<p>The web browser you are using does not support ' +
    '<a href="http://en.wikipedia.org/wiki/Scalable_Vector_Graphics">' +
    'scalable vector graphics</a> (SVG), the technology we use to generate ' +
    'data visualizations.</p>' +
    '<p>Recent versions of most browsers do support SVG, ' +
    'including Firefox, Safari, Chrome, and Opera.</p>' +
    '<p>Microsoft Internet Explorer <b>version 9</b> ' +
    'supports SVG and should work with these web pages.</p>' +
  '</div>';

} else {  // begin SVG dependent code

// The rows and columns of the tests array reflect
// where visualizations are displayed on-screen.
var tests = [[]];
// Start with the first test in the scores data set.
var subj = firstProp(scores),
    grade = firstProp(scores[subj]),
    year = firstProp(scores[subj][grade]);
tests[0][0] = new Test(subj, grade, year, students.all);

function newTestAt(test, row, col, direction) {
  if (row > tests.length - 1) {
    tests[row] = [];
    tests[row][col] = test;
  } else {
    if (col > tests[row].length - 1)
      tests[row][col] = test;
    else {
      if (direction == 'down') {
        tests[tests.length] = [];
        for (var r = tests.length - 2; r >= row; r--) {
          tests[r + 1][col] = tests[r][col];
        }
        tests[row][col] = test;
      }
      else if (direction == 'right') {
        for (var c = tests[row].length - 2; c >= col; c--) {
          tests[row][c + 1] = tests[row][c];
        }
        tests[row][col] = test;
      }
    }
  }
}

function dupDown(node) {
  var i = this.parent.index,
      cols = this.parent.parent.cols(),
      row = Math.floor(i / cols),
      col = i % cols;
  newTestAt(tests[row][col].copy(), row + 1, col, 'down');
  refresh();
  return null;
}
function dupRight(node) {
  var i = this.parent.index,
      cols = this.parent.parent.cols(),
      row = Math.floor(i / cols),
      col = i % cols;
  newTestAt(tests[row][col].copy(), row, col + 1, 'right');
  refresh();
  return null;
}

function refresh() { 
  // Reconstruct data structures and visualizations and render everything.
  tests.forEach(function(row) { row.forEach(function(test) { test.domify() }) });
  visGrid();
  rootPanel.render();
}

refresh();

}  // end of SVG conditional code
